//
//  NeuridionPaper.swift
//  Neuridion Mobile
//
//  A sheet: what goes on paper, and what it looks like.
//
//  --- What a sheet is ---------------------------------------------------
//
//  An **ordered list of blocks between two bands**, exactly as a screen is an
//  ordered list of controls. No dragging, no x/y, no overlapping boxes. The
//  free layout is the tempting answer and the wrong one: it looks perfect in
//  an editor and breaks the moment an address has three lines instead of two,
//  or a company name wraps, or the positions run to twelve rows instead of
//  four. Same decision as the screens, and there it is called the vertical
//  stack.
//
//  --- The one structure -------------------------------------------------
//
//  A first draft had a *positions block*: one table, one key column, one
//  level. That is an invoice block wearing a general name, and Olly said what
//  was wrong with it in one line — "projekte haben jobs und leistungen da
//  wirds komplizierter". So the repetition is a **block that contains
//  blocks**, with the same vocabulary a flow uses: `.repeatRows` names a
//  table and a key column, exactly what `Load Related` and `Add Up` ask for,
//  and nests one deep for Projekt → Job → Leistung.
//
//  Three rules follow, and they explain the whole behaviour:
//
//  1. **A sum inside a repetition counts per group**, one outside counts
//     once. Subtotals are not a feature; they fall out.
//  2. **A repetition holding only a `.tableRow` is drawn as a table** —
//     headings, widths, zebra, carry-over. A drawing rule, not a second
//     concept. Nested, the same block becomes the grouped form.
//  3. **Two levels and no more**, the same limit Areas have and for the same
//     reason: the third is what everybody asks for next and it makes a sheet
//     unreadable.
//
//  --- The look ----------------------------------------------------------
//
//  Five named looks, not six dials. Six *correct* dials are still six
//  decisions before anybody has a sheet; a look sets all of them at once and
//  they stay underneath for whoever wants to adjust. What is left to choose
//  after picking one: the accent, the logo, the density.
//
//  There is no font menu and no colour picker. That is not thrift — it is the
//  reason every sheet looks like it came from the same house.
//

import Foundation
import CoreGraphics

// MARK: - Runs

/// One piece of a line of text: either words, or a placeholder.
///
/// **A chip is not typed text**, and this type is why. It is put in from a
/// menu and is one thing afterwards — not editable, only removable. Mistyping
/// becomes impossible, a placeholder pointing at a column that never existed
/// cannot be written, and a renamed column can carry its chips with it. That
/// is "one meaning" applied to paper.
///
/// Stored as a flat struct rather than an enum with associated values, because
/// every model in this file is hand-written `Codable` and a bool is cheaper to
/// read in a diff than a discriminator.
struct PaperRun: Codable, Hashable {
    /// True when `text` is a placeholder's name rather than words to print.
    var isChip: Bool
    /// The words, or the name being looked up: `Kunde.Firma`, `@Heute`.
    var text: String

    init(_ text: String, chip: Bool = false) {
        self.isChip = chip
        self.text = text
    }

    static func words(_ text: String) -> PaperRun { PaperRun(text) }
    static func chip(_ name: String) -> PaperRun { PaperRun(name, chip: true) }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        isChip = try container.decodeIfPresent(Bool.self, forKey: .isChip) ?? false
        text = try container.decodeIfPresent(String.self, forKey: .text) ?? ""
    }

    private enum CodingKeys: String, CodingKey { case isChip, text }
}

extension Array where Element == PaperRun {
    /// What the editor shows and what a test reads: chips in braces.
    var spelled: String {
        map { $0.isChip ? "{\($0.text)}" : $0.text }.joined()
    }

    /// A line of plain words, for the places a design is built in code.
    static func plain(_ text: String) -> [PaperRun] { [PaperRun(text)] }
}

// MARK: - Columns

/// One column of a table row: what it is headed, which cell it draws, how
/// wide and which way the text sits.
///
/// **Numbers right, text left** — an invoice whose amounts are left-aligned
/// looks wrong before anybody reads it. The width is a share of the table, not
/// millimetres: a sheet has no millimetres anywhere.
struct PaperColumn: Codable, Hashable {
    var heading: String
    /// The record column drawn in this cell.
    var column: String
    /// A share of the table's width. The shares of a row are normalised when
    /// they are drawn, so they never have to add up to one by hand.
    var width: Double
    var isTrailing: Bool

    init(heading: String, column: String, width: Double = 1, trailing: Bool = false) {
        self.heading = heading
        self.column = column
        self.width = width
        self.isTrailing = trailing
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        heading = try container.decodeIfPresent(String.self, forKey: .heading) ?? ""
        column = try container.decodeIfPresent(String.self, forKey: .column) ?? ""
        width = try container.decodeIfPresent(Double.self, forKey: .width) ?? 1
        isTrailing = try container.decodeIfPresent(Bool.self, forKey: .isTrailing) ?? false
    }

    private enum CodingKeys: String, CodingKey { case heading, column, width, isTrailing }
}

// MARK: - Blocks

enum PaperBlockKind: String, Codable, CaseIterable {
    /// The block for the window envelope. The DIN spacing is built in.
    case address
    /// One line, loud.
    case heading
    /// Running text with chips.
    case paragraph
    /// Label on the left, value on the right.
    case valueLine
    /// Names a table and a key column and **contains other blocks**.
    case repeatRows
    /// One row of a table. Only ever inside a repetition.
    case tableRow
    /// A total, a count, an average — the same five `Add Up` has.
    case sum
    /// The VAT breakdown, built from an amount column and a rate column.
    case tax
    /// Five sources, and a code is one of them.
    case image
    /// Air, and optionally a line across it.
    case spacer
    /// From here, a new sheet.
    case pageBreak

    var displayName: String {
        switch self {
        case .address: "Address block"
        case .heading: "Heading"
        case .paragraph: "Paragraph"
        case .valueLine: "Value line"
        case .repeatRows: "Repetition"
        case .tableRow: "Table row"
        case .sum: "Sum"
        case .tax: "Tax"
        case .image: "Picture"
        case .spacer: "Spacer"
        case .pageBreak: "Page break"
        }
    }

    /// The one answer to "does this carry blocks". `ActionVerb.holdsSteps` is
    /// the same question one floor down, and it exists because the branch was
    /// missed in one of the five places that walk a flow.
    var holdsBlocks: Bool { self == .repeatRows }
}

/// What an image block draws. **A code is an image**, generated rather than
/// loaded — which is why there is no code block of its own.
enum PaperImageSource: String, Codable, CaseIterable {
    /// A file in `assets/`, imported the way an app icon is.
    case file
    /// An SF Symbol, told from a file by its extension, as everywhere else.
    case symbol
    /// A cell of an Image column of the row being printed.
    case cell
    /// `CIQRCodeGenerator`. The EPC payment code is this plus a chip text.
    case qr
    /// `CICode128BarcodeGenerator`.
    case barcode
}

enum PaperTally: String, Codable, CaseIterable {
    case total, count, average, smallest, largest

    var displayName: String {
        switch self {
        case .total: "Total"
        case .count: "How many"
        case .average: "Average"
        case .smallest: "Smallest"
        case .largest: "Largest"
        }
    }
}

/// One block of a sheet.
///
/// A single type with every setting on it rather than eleven types, for the
/// reason `ActionRule` is one type with every gap on it: the list has to be
/// homogeneous to be reordered, archived and walked, and a block with an
/// unused field costs one key in a JSON file.
final class PaperBlock: Codable, Identifiable {
    var id: UUID
    var kind: PaperBlockKind

    /// `.heading`, `.paragraph` — and the text of a `.qr`, which is a template
    /// of chips like everything else. The EPC code is twelve lines of exactly
    /// that and needs no concept of its own.
    var text: [PaperRun]
    /// `.valueLine`, `.sum`, `.tax`, `.address` — the words on the left.
    var label: String
    /// `.valueLine` — the value on the right.
    var value: [PaperRun]

    /// `.repeatRows` — which table, and the column its rows are matched on.
    /// Exactly the two facts `Load Related` needs.
    var table: String
    var keyColumn: String
    /// `.repeatRows` — the value the key is matched against, usually a chip.
    var keyValue: [PaperRun]

    /// `.tableRow`
    var columns: [PaperColumn]

    /// `.sum` — the column added up. `.tax` — the amount column.
    var column: String
    /// `.tax` — the column holding the rate, so mixed rates on one invoice
    /// come out as separate lines. Sum blocks alone cannot do that at all,
    /// which is why the tax block exists.
    var rateColumn: String
    var tally: PaperTally
    /// Drawn heavier, with a rule above it. The number somebody came to see.
    var isEmphasised: Bool

    /// `.image`
    var source: PaperImageSource
    /// The file name, the symbol name, or the column, depending on `source`.
    var imageName: String
    /// A share of the text width.
    var imageWidth: Double

    /// `.spacer` — how much air, and whether a line sits in it.
    var height: Double
    var hasRule: Bool

    /// `.address` — the lines of the address, each a list of runs. A line
    /// whose chips are all empty still leaves a gap: Olly's call, and the same
    /// rule everywhere else on the sheet.
    var lines: [[PaperRun]]

    /// `.repeatRows` only. Two levels; the third is refused when it is built,
    /// not when it is drawn.
    var children: [PaperBlock]

    init(_ kind: PaperBlockKind, id: UUID = UUID()) {
        self.id = id
        self.kind = kind
        self.text = []
        self.label = ""
        self.value = []
        self.table = ""
        self.keyColumn = ""
        self.keyValue = []
        self.columns = []
        self.column = ""
        self.rateColumn = ""
        self.tally = .total
        self.isEmphasised = false
        self.source = .file
        self.imageName = ""
        self.imageWidth = 0.25
        self.height = 10
        self.hasRule = false
        self.lines = []
        self.children = []
    }

    /// How deep this block's own children go. Used where the limit is
    /// enforced — when a block is added, never when it is drawn.
    var depth: Int {
        guard kind.holdsBlocks else { return 0 }
        return 1 + (children.map(\.depth).max() ?? 0)
    }

    /// The drawing rule that keeps the good invoice table: **a repetition
    /// holding nothing but one table row is a table**, with headings, widths,
    /// zebra and a carry-over. Nested, the same block is the grouped form.
    var drawsAsTable: Bool {
        kind.holdsBlocks && children.count == 1 && children[0].kind == .tableRow
    }

    // MARK: Codable

    private enum CodingKeys: String, CodingKey {
        case id, kind, text, label, value, table, keyColumn, keyValue, columns,
             column, rateColumn, tally, isEmphasised, source, imageName,
             imageWidth, height, hasRule, lines, children
    }

    init(from decoder: Decoder) throws {
        let c = try decoder.container(keyedBy: CodingKeys.self)
        id = try c.decodeIfPresent(UUID.self, forKey: .id) ?? UUID()
        kind = try c.decodeIfPresent(PaperBlockKind.self, forKey: .kind) ?? .paragraph
        text = try c.decodeIfPresent([PaperRun].self, forKey: .text) ?? []
        label = try c.decodeIfPresent(String.self, forKey: .label) ?? ""
        value = try c.decodeIfPresent([PaperRun].self, forKey: .value) ?? []
        table = try c.decodeIfPresent(String.self, forKey: .table) ?? ""
        keyColumn = try c.decodeIfPresent(String.self, forKey: .keyColumn) ?? ""
        keyValue = try c.decodeIfPresent([PaperRun].self, forKey: .keyValue) ?? []
        columns = try c.decodeIfPresent([PaperColumn].self, forKey: .columns) ?? []
        column = try c.decodeIfPresent(String.self, forKey: .column) ?? ""
        rateColumn = try c.decodeIfPresent(String.self, forKey: .rateColumn) ?? ""
        tally = try c.decodeIfPresent(PaperTally.self, forKey: .tally) ?? .total
        isEmphasised = try c.decodeIfPresent(Bool.self, forKey: .isEmphasised) ?? false
        source = try c.decodeIfPresent(PaperImageSource.self, forKey: .source) ?? .file
        imageName = try c.decodeIfPresent(String.self, forKey: .imageName) ?? ""
        imageWidth = try c.decodeIfPresent(Double.self, forKey: .imageWidth) ?? 0.25
        height = try c.decodeIfPresent(Double.self, forKey: .height) ?? 10
        hasRule = try c.decodeIfPresent(Bool.self, forKey: .hasRule) ?? false
        lines = try c.decodeIfPresent([[PaperRun]].self, forKey: .lines) ?? []
        children = try c.decodeIfPresent([PaperBlock].self, forKey: .children) ?? []
    }
}

// MARK: - The look

/// Five named looks. Each sets everything at once.
enum PaperLook: String, Codable, CaseIterable {
    case modern, classic, ledger, workshop, historic

    var displayName: String {
        switch self {
        case .modern: "Modern"
        case .classic: "Classic"
        case .ledger: "Ledger"
        case .workshop: "Workshop"
        case .historic: "Historic"
        }
    }

    /// Who it is for, said in the words somebody would use to pick.
    var about: String {
        switch self {
        case .modern: "Accent bar, coloured heading, airy — agencies, software"
        case .classic: "Centred heading, no colour, fine rules — law, notaries"
        case .ledger: "Set tight, zebra rows, all on one page — trade, accounts"
        case .workshop: "Strong accent, big figures, clear rules — trades, building"
        case .historic: "Wide margins, black only, small caps — deeds, certificates"
        }
    }

    var isSerif: Bool { self == .classic || self == .historic }
    /// Whether the accent colour is used at all. Two looks are deliberately
    /// black and white, and choosing an accent in them changes nothing.
    var usesAccent: Bool { self != .classic && self != .historic }
    var showsAccentBar: Bool { self == .modern || self == .workshop }
    var centresHeading: Bool { self == .classic || self == .historic }

    enum TableStyle: String, Codable { case rules, zebra, plain }
    var tableStyle: TableStyle {
        switch self {
        case .modern: .rules
        case .classic: .rules
        case .ledger: .zebra
        case .workshop: .rules
        case .historic: .plain
        }
    }

    /// Body size in points before the density is applied.
    var bodySize: CGFloat {
        switch self {
        case .ledger: 8.5
        case .workshop: 10
        default: 9.5
        }
    }

    var headingSize: CGFloat { self == .workshop ? 20 : 17 }

    /// The page margin. The historic look is wide on purpose; the ledger is
    /// narrow because its whole point is fitting on one page.
    var margin: CGFloat {
        switch self {
        case .historic: 76
        case .ledger: 46
        default: 56
        }
    }
}

enum PaperDensity: String, Codable, CaseIterable {
    case tight, normal, airy

    var displayName: String {
        switch self {
        case .tight: "tight"
        case .normal: "normal"
        case .airy: "airy"
        }
    }

    /// One dial for size, leading and margin together — three separate ones
    /// mean setting three things and hitting none.
    var scale: CGFloat {
        switch self {
        case .tight: 0.88
        case .normal: 1
        case .airy: 1.14
        }
    }
}

// MARK: - The bands

/// The head. Left who you are, right how you are reached — because every
/// letterhead in the world is built that way.
struct PaperHead: Codable {
    var isOn: Bool
    /// A file in `assets/` or an SF Symbol. Empty draws the name alone.
    var logo: String
    var name: String
    /// The line under the name.
    var line: String
    /// Up to five short lines on the right: address, telephone, mail, VAT id.
    var contact: [String]

    init(isOn: Bool = true, logo: String = "", name: String = "",
         line: String = "", contact: [String] = []) {
        self.isOn = isOn
        self.logo = logo
        self.name = name
        self.line = line
        self.contact = contact
    }

    init(from decoder: Decoder) throws {
        let c = try decoder.container(keyedBy: CodingKeys.self)
        isOn = try c.decodeIfPresent(Bool.self, forKey: .isOn) ?? true
        logo = try c.decodeIfPresent(String.self, forKey: .logo) ?? ""
        name = try c.decodeIfPresent(String.self, forKey: .name) ?? ""
        line = try c.decodeIfPresent(String.self, forKey: .line) ?? ""
        contact = try c.decodeIfPresent([String].self, forKey: .contact) ?? []
    }

    private enum CodingKeys: String, CodingKey { case isOn, logo, name, line, contact }
}

/// The foot, in three columns — because German small print *is* three columns:
/// company and registry, bank, management. A one-line foot forces separators
/// nobody reads.
struct PaperFoot: Codable {
    var isOn: Bool
    var columns: [[String]]
    var showsPageNumber: Bool

    init(isOn: Bool = true, columns: [[String]] = [], showsPageNumber: Bool = true) {
        self.isOn = isOn
        self.columns = columns
        self.showsPageNumber = showsPageNumber
    }

    init(from decoder: Decoder) throws {
        let c = try decoder.container(keyedBy: CodingKeys.self)
        isOn = try c.decodeIfPresent(Bool.self, forKey: .isOn) ?? true
        columns = try c.decodeIfPresent([[String]].self, forKey: .columns) ?? []
        showsPageNumber = try c.decodeIfPresent(Bool.self, forKey: .showsPageNumber) ?? true
    }

    private enum CodingKeys: String, CodingKey { case isOn, columns, showsPageNumber }
}

// MARK: - The sheet

final class NeuridionPaper: Codable, Identifiable {
    var id: UUID
    /// What it is called in the step's menu.
    var name: String

    /// **The table this sheet is about**, named once here rather than at every
    /// step — the way a list box binds one. Everything about where it may be
    /// printed follows from it: *the record on this screen* and *the rows the
    /// list has found* need the screen to be showing this table; *every row*
    /// needs nothing, which is the report case.
    var tableName: String

    var look: PaperLook
    /// The first of the eight an area may be.
    ///
    /// Written out rather than `AppArea.palette[0]`, and that is the one
    /// concession this file makes to being copied: it travels verbatim into
    /// exported apps (`Neuridion Mobile/Export`), so it may not name anything
    /// outside itself. `PaperTests` checks that it is still the first of the
    /// eight, which is the whole of what the reference bought.
    static let firstAccent = "3D8BFF"

    /// One of `AppArea.palette`. A colour picker would be the ninth way to
    /// miss grey.
    var accent: String
    var density: PaperDensity

    var head: PaperHead
    var foot: PaperFoot
    var blocks: [PaperBlock]

    /// Diagonal, pale, behind the text: "", "ENTWURF", "KOPIE", "BEZAHLT" or
    /// whatever somebody types. A draft quote that is not recognisable as a
    /// draft gets paid.
    var stamp: String
    /// Two hairlines at 105 and 210 mm and a punch mark. Five lines of code,
    /// and without them nobody folds a letter straight.
    var showsMarks: Bool

    init(id: UUID = UUID(), name: String, tableName: String = "",
         look: PaperLook = .modern, accent: String = NeuridionPaper.firstAccent,
         density: PaperDensity = .normal,
         head: PaperHead = PaperHead(), foot: PaperFoot = PaperFoot(),
         blocks: [PaperBlock] = [], stamp: String = "", showsMarks: Bool = true) {
        self.id = id
        self.name = name
        self.tableName = tableName
        self.look = look
        self.accent = accent
        self.density = density
        self.head = head
        self.foot = foot
        self.blocks = blocks
        self.stamp = stamp
        self.showsMarks = showsMarks
    }

    /// Every block, including the ones inside a repetition.
    ///
    /// **Careful: every walk over "all the blocks" has to go into the
    /// children.** `SwiftGenerator.flattened` exists for exactly this one
    /// floor down, and it exists because the branch was missed in one of the
    /// five places that walk a flow — and a step inside an arm that a pass
    /// never saw silently lost its state property.
    var flattened: [PaperBlock] {
        func walk(_ blocks: [PaperBlock]) -> [PaperBlock] {
            blocks.flatMap { [$0] + walk($0.children) }
        }
        return walk(blocks)
    }

    /// Deepest nesting anywhere on the sheet. One means a plain table.
    var depth: Int { blocks.map(\.depth).max() ?? 0 }

    /// Two levels and no more, checked where a block is added.
    static let deepest = 2

    private enum CodingKeys: String, CodingKey {
        case id, name, tableName, look, accent, density, head, foot, blocks,
             stamp, showsMarks
    }

    init(from decoder: Decoder) throws {
        let c = try decoder.container(keyedBy: CodingKeys.self)
        id = try c.decodeIfPresent(UUID.self, forKey: .id) ?? UUID()
        name = try c.decodeIfPresent(String.self, forKey: .name) ?? "Sheet"
        tableName = try c.decodeIfPresent(String.self, forKey: .tableName) ?? ""
        look = try c.decodeIfPresent(PaperLook.self, forKey: .look) ?? .modern
        accent = try c.decodeIfPresent(String.self, forKey: .accent) ?? NeuridionPaper.firstAccent
        density = try c.decodeIfPresent(PaperDensity.self, forKey: .density) ?? .normal
        head = try c.decodeIfPresent(PaperHead.self, forKey: .head) ?? PaperHead()
        foot = try c.decodeIfPresent(PaperFoot.self, forKey: .foot) ?? PaperFoot()
        blocks = try c.decodeIfPresent([PaperBlock].self, forKey: .blocks) ?? []
        stamp = try c.decodeIfPresent(String.self, forKey: .stamp) ?? ""
        showsMarks = try c.decodeIfPresent(Bool.self, forKey: .showsMarks) ?? true
    }
}
